--- @since 25.4.4

local WINDOWS = ya.target_family() == "windows"

-- 定义支持的 Git 状态代码，用于确定目录中包含不同状态时显示哪种状态
-- 也用于 bubble_up 函数中处理目录状态
local CODES = {
	excluded = 100, -- 被排除的目录（忽略的目录）
	ignored = 6, -- 被忽略的文件
	untracked = 5, -- 未跟踪的文件
	modified = 4, -- 修改的文件
	added = 3, -- 已添加到暂存区的文件
	deleted = 2, -- 已删除的文件
	updated = 1, -- 已更新的文件
	unknown = 0, -- 未知状态
}

-- 定义匹配 Git 状态行的模式和对应的状态代码
-- 用于从 git status 的输出中提取文件状态
local PATTERNS = {
	{ "!$", CODES.ignored },          -- 匹配被忽略的文件
	{ "?$", CODES.untracked },        -- 匹配未跟踪的文件
	{ "[MT]", CODES.modified },       -- 匹配修改的文件（M）或类型改变的文件（T）
	{ "[AC]", CODES.added },          -- 匹配已添加的文件（A）或从缓存中复制的文件（C）
	{ "D", CODES.deleted },           -- 匹配已删除的文件
	{ "U", CODES.updated },           -- 匹配已更新的文件（但未合并）
	{ "[AD][AD]", CODES.updated },    -- 匹配添加后又删除或删除后又添加的文件
}

-- 匹配 git status 输出行，提取文件路径和状态代码
local function match(line)
	local signs = line:sub(1, 2) -- 获取状态标识符（前两个字符）
	for _, p in ipairs(PATTERNS) do
		local path, pattern, code = nil, p[1], p[2]
		if signs:find(pattern) then
			-- 提取文件路径，处理带引号的路径（Windows 风格）
			path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4)
			-- 根据操作系统调整路径分隔符
			path = WINDOWS and path:gsub("/", "\\") or path
		end
		if not path then
			-- 如果未匹配到路径，跳过
		elseif path:find("[/\\]$") then
			-- 如果路径是目录且状态为 ignored，则标记为 excluded
			return code == CODES.ignored and CODES.excluded or code, path:sub(1, -2)
		else
			-- 返回文件状态代码和路径
			return code, path
		end
	end
end

-- 查找当前工作目录的 Git 仓库根目录
local function root(cwd)
	local is_worktree = function(url)
		local file, head = io.open(tostring(url)), nil
		if file then
			head = file:read(8)
			file:close()
		end
		-- 检查是否是工作树（gitdir: 开头）
		return head == "gitdir: "
	end

	repeat
		local next = cwd:join(".git") -- 获取 .git 目录路径
		local cha = fs.cha(next) -- 检查 .git 目录是否存在
		if cha and (cha.is_dir or is_worktree(next)) then
			-- 如果找到 .git 目录或工作树配置文件，返回当前目录作为仓库根目录
			return tostring(cwd)
		end
		-- 向上一级目录查找
		cwd = cwd.parent
	until not cwd
end

-- 将文件状态向上冒泡到父目录
local function bubble_up(changed)
	local new, empty = {}, Url("") -- 新状态表和空 URL
	for path, code in pairs(changed) do
		if code ~= CODES.ignored then
			-- 获取文件的父目录
			local url = Url(path).parent
			while url and url ~= empty do
				-- 父目录状态取子目录/文件状态的最大值
				new[tostring(url)] = (new[tostring(url)] or CODES.unknown) > code and new[tostring(url)] or code
				url = url.parent -- 继续向上一级目录
			end
		end
	end
	return new -- 返回处理后的目录状态
end

-- 向下传播排除目录的状态
local function propagate_down(excluded, cwd, repo)
	local new, rel = {}, cwd:strip_prefix(repo) -- 新状态表和相对路径
	for _, path in ipairs(excluded) do
		if rel:starts_with(path) then
			-- 如果当前目录是排除目录的子目录，标记为 excluded
			new[tostring(cwd)] = CODES.excluded
		elseif cwd == repo:join(path).parent then
			-- 如果路径是当前目录的直接子目录，标记为 ignored
			new[path] = CODES.ignored
		else
			-- 忽略不相关的路径
		end
	end
	return new -- 返回处理后的排除目录状态
end

-- 同步函数：添加 Git 状态到 yazi 状态管理
local add = ya.sync(function(st, cwd, repo, changed)
	st.dirs[cwd] = repo -- 将目录与仓库关联
	st.repos[repo] = st.repos[repo] or {} -- 确保仓库状态表存在
	for path, code in pairs(changed) do
		if code == CODES.unknown then
			-- 清除未知状态
			st.repos[repo][path] = nil
		elseif code == CODES.excluded then
			-- 标记排除目录
			st.dirs[path] = CODES.excluded
		else
			-- 更新文件状态
			st.repos[repo][path] = code
		end
	end
	ya.render() -- 触发界面重绘
end)

-- 同步函数：从 yazi 状态管理中移除 Git 状态
local remove = ya.sync(function(st, cwd)
	local repo = st.dirs[cwd]
	if not repo then
		return -- 如果目录未关联仓库，直接返回
	end

	ya.render() -- 触发界面重绘
	st.dirs[cwd] = nil -- 移除目录与仓库的关联
	if not st.repos[repo] then
		return
	end

	-- 检查仓库是否还有关联目录
	for _, r in pairs(st.dirs) do
		if r == repo then
			return -- 如果还有关联目录，保留仓库状态
		end
	end
	st.repos[repo] = nil -- 移除仓库状态
end)

-- 设置插件（初始化）
local function setup(st, opts)
	st.dirs = {} -- 目录到仓库的映射表
	st.repos = {} -- 仓库到文件状态的映射表

	opts = opts or {}
	opts.order = opts.order or 1500 -- 默认渲染顺序

	local t = th.git or {} -- 获取主题配置
	local styles = {
		[CODES.ignored] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("darkgray"), -- 被忽略文件样式
		[CODES.untracked] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("magenta"), -- 未跟踪文件样式
		[CODES.modified] = t.modified and ui.Style(t.modified) or ui.Style():fg("yellow"), -- 修改文件样式
		[CODES.added] = t.added and ui.Style(t.added) or ui.Style():fg("green"), -- 已添加文件样式
		[CODES.deleted] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("red"), -- 已删除文件样式
		[CODES.updated] = t.updated and ui.Style(t.updated) or ui.Style():fg("yellow"), -- 已更新文件样式
	}
	local signs = {
		-- [CODES.ignored] = t.ignored_sign or "", -- 被忽略文件图标
		-- [CODES.untracked] = t.untracked_sign or "?", -- 未跟踪文件图标
		-- [CODES.modified] = t.modified_sign or "", -- 修改文件图标
		-- [CODES.added] = t.added_sign or "", -- 已添加文件图标
		-- [CODES.deleted] = t.deleted_sign or "", -- 已删除文件图标
		-- [CODES.updated] = t.updated_sign or "", -- 已更新文件图标
		[CODES.ignored] = t.ignored_sign or "!", -- 被忽略文件图标
		[CODES.untracked] = t.untracked_sign or "?", -- 未跟踪文件图标
		[CODES.modified] = t.modified_sign or "[MT]", -- 修改文件图标
		[CODES.added] = t.added_sign or "[AC]", -- 已添加文件图标
		[CODES.deleted] = t.deleted_sign or "D", -- 已删除文件图标
		[CODES.updated] = t.updated_sign or "U", -- 已更新文件图标

	}

	-- 添加 Linemode 子元素，渲染 Git 状态图标
	Linemode:children_add(function(self)
		local url = self._file.url -- 当前文件 URL
		local repo = st.dirs[tostring(url.base)] -- 获取关联仓库
		local code
		if repo then
			-- 如果是排除目录，使用特殊值标记
			code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)]
		end

		if not code or signs[code] == "" then
			-- 如果没有状态或图标为空，不渲染
			return ""
		elseif self._file.is_hovered then
			-- 如果文件被悬停，渲染纯文本图标
			return ui.Line { " ", signs[code] }
		else
			-- 渲染带样式的图标
			return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) }
		end
	end, opts.order)
end

-- 获取 Git 状态
local function fetch(_, job)
	local cwd = job.files[1].url.base -- 获取当前工作目录
	local repo = root(cwd) -- 查找 Git 仓库根目录
	if not repo then
		remove(tostring(cwd)) -- 如果不是仓库目录，移除状态
		return true
	end

	local paths = {} -- 收集需要检查的文件路径
	for _, file in ipairs(job.files) do
		paths[#paths + 1] = tostring(file.url)
	end

	-- 执行 git status 命令，获取文件状态
	local output, err = Command("git")
		:cwd(tostring(cwd)) -- 设置工作目录
		:args({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" }) -- Git 命令参数
		:args(paths) -- 添加文件路径参数
		:stdout(Command.PIPED) -- 设置标准输出为管道
		:output() -- 执行命令并获取输出
	if not output then
		return true, Err("Cannot spawn `git` command, error: %s", err) -- 如果命令失败，返回错误
	end

	local changed, excluded = {}, {} -- 初始化状态表和排除目录表
	for line in output.stdout:gmatch("[^\r\n]+") do -- 遍历 Git 命令输出的每一行
		local code, path = match(line) -- 匹配状态行
		if code == CODES.excluded then
			excluded[#excluded + 1] = path -- 收集排除目录
		else
			changed[path] = code -- 收集文件状态
		end
	end

	if job.files[1].cha.is_dir then
		-- 如果是目录，将状态向上冒泡到父目录
		ya.dict_merge(changed, bubble_up(changed))
	end
	-- 向下传播排除目录的状态
	ya.dict_merge(changed, propagate_down(excluded, cwd, Url(repo)))

	-- 重置未出现在 Git 状态输出中的文件状态为未知
	for _, path in ipairs(paths) do
		local s = path:sub(#repo + 2)
		changed[s] = changed[s] or CODES.unknown
	end

	add(tostring(cwd), repo, changed) -- 更新 yazi 状态

	return false -- 表示处理成功
end

-- 返回插件的 setup 和 fetch 函数
return { setup = setup, fetch = fetch }